﻿//------------------------------------------------------------
// Game Framework
// Copyright © 2013-2020 Jiang Yin. All rights reserved.
// Homepage: https://gameframework.cn/
// Feedback: mailto:ellan@gameframework.cn
//------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;
using Core;
using UnityEditor;
using UnityEngine;

namespace UnityGameFramework.Editor.ResourceTools
{
    /// <summary>
    /// 资源集合。
    /// </summary>
    public sealed class ResourceBuilderCollection
    {
        private const string SceneExtension = ".unity";
        private static readonly Regex ResourceNameRegex = new Regex(@"^([A-Za-z0-9\._-]+/)*[A-Za-z0-9\._-]+$");
        private static readonly Regex ResourceVariantRegex = new Regex(@"^[a-z0-9_-]+$");

        private readonly string m_ConfigurationPath;
        private readonly SortedDictionary<string, Resource> m_Resources;
        private readonly SortedDictionary<string, Asset> m_Assets;

        public ResourceBuilderCollection()
        {
            m_ConfigurationPath = Type.GetConfigurationPath<ResourceCollectionConfigPathAttribute>() ??
                                  Utils.File.GetRegularPath(Path.Combine(Application.dataPath,
                                      "GameFramework/Configs/ResourceCollection.xml"));
            m_Resources = new SortedDictionary<string, Resource>(StringComparer.Ordinal);
            m_Assets = new SortedDictionary<string, Asset>(StringComparer.Ordinal);
        }

        public int ResourceCount => m_Resources.Count;

        public int AssetCount => m_Assets.Count;


        public event Action<int, int> OnLoadingResource;

        public event Action<int, int> OnLoadingAsset;

        public event Action<int, int> OnLoadingFolder;

        public event Action OnLoadCompleted;

        public event Func<string, bool> CheckAssetInController;

        public event Func<string, bool> CheckFolderInController;

        public void Clear()
        {
            m_Resources.Clear();
            m_Assets.Clear();
        }

        public bool Load()
        {
            Clear();

            if (!File.Exists(m_ConfigurationPath))
            {
                return false;
            }

            try
            {
                XmlDocument xmlDocument = new XmlDocument();
                xmlDocument.Load(m_ConfigurationPath);
                XmlNode xmlRoot = xmlDocument.SelectSingleNode("UnityGameFramework");
                XmlNode xmlCollection = xmlRoot.SelectSingleNode("ResourceCollection");
                XmlNode xmlResources = xmlCollection.SelectSingleNode("Resources");
                XmlNode xmlAssets = xmlCollection.SelectSingleNode("Assets");
                XmlNode xmlFolders = xmlCollection.SelectSingleNode("Folders");

                XmlNodeList xmlNodeList = null;
                XmlNode xmlNode = null;
                int count = 0;

                xmlNodeList = xmlResources.ChildNodes;
                count = xmlNodeList.Count;
                for (int i = 0; i < count; i++)
                {
                    if (OnLoadingResource != null)
                    {
                        OnLoadingResource(i, count);
                    }

                    xmlNode = xmlNodeList.Item(i);
                    if (xmlNode.Name != "Resource")
                    {
                        continue;
                    }

                    string name = xmlNode.Attributes.GetNamedItem("Name").Value;

                    string variant = xmlNode.Attributes.GetNamedItem("Variant") != null
                        ? xmlNode.Attributes.GetNamedItem("Variant").Value
                        : null;
                    //string fileSystem = xmlNode.Attributes.GetNamedItem("FileSystem") != null ? xmlNode.Attributes.GetNamedItem("FileSystem").Value : null;
                    string fileSystem = $"VFS_{name.Replace("\\", "_").Replace("/", "_")}";
                    byte loadType = 0;
                    if (xmlNode.Attributes.GetNamedItem("LoadType") != null)
                    {
                        byte.TryParse(xmlNode.Attributes.GetNamedItem("LoadType").Value, out loadType);
                    }

                    bool packed = false;
                    if (xmlNode.Attributes.GetNamedItem("Packed") != null)
                    {
                        bool.TryParse(xmlNode.Attributes.GetNamedItem("Packed").Value, out packed);
                    }

                    bool singleBuild = false;
                    if (xmlNode.Attributes.GetNamedItem("SingleBuild") != null)
                    {
                        bool.TryParse(xmlNode.Attributes.GetNamedItem("SingleBuild").Value, out singleBuild);
                    }

                    string[] resourceGroups = xmlNode.Attributes.GetNamedItem("ResourceGroups") != null
                        ? xmlNode.Attributes.GetNamedItem("ResourceGroups").Value.Split(',')
                        : null;
                    if (!AddResource(name, variant, fileSystem, (LoadType)loadType, packed, resourceGroups,
                            singleBuild))
                    {
                        Debug.LogWarning(Utils.Text.Format("Can not add resource '{0}'.",
                            GetResourceFullName(name, variant)));
                        continue;
                    }
                }

                xmlNodeList = xmlAssets.ChildNodes;
                count = xmlNodeList.Count;
                for (int i = 0; i < count; i++)
                {
                    OnLoadingAsset?.Invoke(i, count);

                    xmlNode = xmlNodeList.Item(i);
                    if (xmlNode.Name != "Asset")
                    {
                        continue;
                    }

                    string guid = xmlNode.Attributes.GetNamedItem("Guid").Value;
                    string name = xmlNode.Attributes.GetNamedItem("ResourceName").Value;
                    string variant = xmlNode.Attributes.GetNamedItem("ResourceVariant") != null
                        ? xmlNode.Attributes.GetNamedItem("ResourceVariant").Value
                        : null;
                    if (CheckAssetInController?.Invoke(guid) == false)
                    {
                        Debug.LogError($"{name} 里存在missing asset!");
                    }

                    if (!AssignAsset(guid, name, variant))
                    {
                        Debug.LogWarning(Utils.Text.Format("Can not assign asset '{0}' to resource '{1}'.", guid,
                            GetResourceFullName(name, variant)));
                        continue;
                    }
                }

                xmlNodeList = xmlFolders.ChildNodes;
                count = xmlNodeList.Count;
                for (int i = 0; i < count; i++)
                {
                    OnLoadingFolder?.Invoke(i, count);

                    xmlNode = xmlNodeList.Item(i);
                    if (xmlNode.Name != "Folder")
                    {
                        continue;
                    }

                    string path = xmlNode.Attributes.GetNamedItem("Path").Value;
                    string name = xmlNode.Attributes.GetNamedItem("ResourceName").Value;
                    string variant = xmlNode.Attributes.GetNamedItem("ResourceVariant") != null
                        ? xmlNode.Attributes.GetNamedItem("ResourceVariant").Value
                        : null;
                    if (CheckFolderInController?.Invoke(path) == false)
                    {
                        Debug.LogError($"{name} 里存在missing folder!");
                    }

                    if (!AssignAssetByFolder(path, name, variant))
                    {
                        Debug.LogWarning(Utils.Text.Format("Can not assign folder '{0}' to resource '{1}'.", path,
                            GetResourceFullName(name, variant)));
                        continue;
                    }
                }

                OnLoadCompleted?.Invoke();

                return true;
            }
            catch
            {
                File.Delete(m_ConfigurationPath);
                if (OnLoadCompleted != null)
                {
                    OnLoadCompleted();
                }

                return false;
            }
        }

        public Resource[] GetResources()
        {
            return m_Resources.Values.ToArray();
        }

        public Resource GetResource(string name, string variant)
        {
            if (!IsValidResourceName(name, variant))
            {
                return null;
            }

            Resource resource = null;
            if (m_Resources.TryGetValue(GetResourceFullName(name, variant).ToLower(), out resource))
            {
                return resource;
            }

            return null;
        }

        public bool HasResource(string name, string variant)
        {
            if (!IsValidResourceName(name, variant))
            {
                return false;
            }

            return m_Resources.ContainsKey(GetResourceFullName(name, variant).ToLower());
        }

        public bool AddResource(string name, string variant, string fileSystem, LoadType loadType, bool packed)
        {
            return AddResource(name, variant, fileSystem, loadType, packed, null);
        }

        public bool AddResource(string name, string variant, string fileSystem, LoadType loadType, bool packed,
            string[] resourceGroups, bool singleBuild = false)
        {
            if (!IsValidResourceName(name, variant))
            {
                return false;
            }

            if (!IsAvailableResourceName(name, variant, null))
            {
                return false;
            }

            if (fileSystem != null && !ResourceNameRegex.IsMatch(fileSystem))
            {
                return false;
            }

            Resource resource =
                Resource.Create(name, variant, fileSystem, loadType, packed, resourceGroups, singleBuild);
            m_Resources.Add(resource.FullName.ToLower(), resource);

            return true;
        }


        public bool SetResourceLoadType(string name, string variant, LoadType loadType)
        {
            if (!IsValidResourceName(name, variant))
            {
                return false;
            }

            Resource resource = GetResource(name, variant);
            if (resource == null)
            {
                return false;
            }

            if ((loadType == LoadType.LoadFromBinary || loadType == LoadType.LoadFromBinaryAndQuickDecrypt ||
                 loadType == LoadType.LoadFromBinaryAndDecrypt) && resource.GetAssets().Length > 1)
            {
                return false;
            }

            resource.LoadType = loadType;
            return true;
        }

        public bool SetResourcePacked(string name, string variant, bool packed)
        {
            if (!IsValidResourceName(name, variant))
            {
                return false;
            }

            Resource resource = GetResource(name, variant);
            if (resource == null)
            {
                return false;
            }

            resource.Packed = packed;
            return true;
        }

        public Asset[] GetAssets()
        {
            return m_Assets.Values.ToArray();
        }

        public Asset[] GetAssets(string name, string variant)
        {
            if (!IsValidResourceName(name, variant))
            {
                return new Asset[0];
            }

            Resource resource = GetResource(name, variant);
            if (resource == null)
            {
                return new Asset[0];
            }

            return resource.GetAssets();
        }

        public Asset GetAsset(string guid)
        {
            if (string.IsNullOrEmpty(guid))
            {
                return null;
            }

            Asset asset = null;
            if (m_Assets.TryGetValue(guid, out asset))
            {
                return asset;
            }

            return null;
        }

        public bool HasAsset(string guid)
        {
            if (string.IsNullOrEmpty(guid))
            {
                return false;
            }

            return m_Assets.ContainsKey(guid);
        }

        public bool AssignAsset(string guid, string name, string variant)
        {
            if (string.IsNullOrEmpty(guid))
            {
                return false;
            }

            if (!IsValidResourceName(name, variant))
            {
                return false;
            }

            Resource resource = GetResource(name, variant);
            if (resource == null)
            {
                return false;
            }

            string assetName = AssetDatabase.GUIDToAssetPath(guid);
            if (string.IsNullOrEmpty(assetName))
            {
                return false;
            }

            Asset[] assetsInResource = resource.GetAssets();
            foreach (Asset assetInResource in assetsInResource)
            {
                if (assetInResource.Path == assetName)
                {
                    continue;
                }

                if (assetInResource.Path.ToLower() == assetName.ToLower())
                {
                    return false;
                }
            }

            bool isScene = assetName.EndsWith(SceneExtension, StringComparison.Ordinal);
            if (isScene && resource.AssetType == AssetType.Asset || !isScene && resource.AssetType == AssetType.Scene)
            {
                return false;
            }

            Asset asset = GetAsset(guid);
            if (resource.IsLoadFromBinary && assetsInResource.Length > 0 && asset != assetsInResource[0])
            {
                return false;
            }

            if (asset == null)
            {
                asset = Asset.Create(guid, assetName);
                m_Assets.Add(asset.Guid, asset);
            }

            resource.AssignAsset(asset, isScene);

            return true;
        }

        public bool UnassignAsset(string guid)
        {
            if (string.IsNullOrEmpty(guid))
            {
                return false;
            }

            Asset asset = GetAsset(guid);
            if (asset != null)
            {
                if (asset.Resource != null)
                {
                    asset.Resource.UnassignAsset(asset);
                }

                m_Assets.Remove(asset.Guid);
            }

            return true;
        }


        public bool AssignAssetByFolder(string path, string name, string varient)
        {
            var absolute_path = Path.Combine(Application.dataPath, path);
            if (Directory.Exists(absolute_path) == false)
            {
                Debug.LogError($"Folder Is Not Exist; Path = {absolute_path}");
                return false;
            }

            DirectoryInfo di = new DirectoryInfo(absolute_path);
            var files = di.GetFiles();

            foreach (var f in files)
            {
                if (f.Extension == ".meta")
                {
                    continue;
                }

                var p = Path.Combine(Path.Combine("Assets", path), f.Name);
                var guid = AssetDatabase.AssetPathToGUID(p);
                AssignAsset(guid, name, varient);
                //Debug.LogError(p);
                //Debug.LogError(guid);
            }

            var dis = di.GetDirectories();

            bool result = true;
            foreach (var p in dis)
            {
                result = AssignAssetByFolder(Path.Combine(path, p.Name), name, varient);
                if (result == false)
                {
                    return false;
                }
            }

            return true;
        }

        private string GetResourceFullName(string name, string variant)
        {
            return !string.IsNullOrEmpty(variant) ? Utils.Text.Format("{0}.{1}", name, variant) : name;
        }

        private bool IsValidResourceName(string name, string variant)
        {
            if (string.IsNullOrEmpty(name))
            {
                return false;
            }

            if (!ResourceNameRegex.IsMatch(name))
            {
                return false;
            }

            if (variant != null && !ResourceVariantRegex.IsMatch(variant))
            {
                return false;
            }

            return true;
        }

        private bool IsAvailableResourceName(string name, string variant, Resource current)
        {
            Resource found = GetResource(name, variant);
            if (found != null && found != current)
            {
                return false;
            }

            string[] foundPathNames = name.Split('/');
            foreach (Resource resource in m_Resources.Values)
            {
                if (current != null && resource == current)
                {
                    continue;
                }

                if (resource.Name == name)
                {
                    if (resource.Variant == null && variant != null)
                    {
                        return false;
                    }

                    if (resource.Variant != null && variant == null)
                    {
                        return false;
                    }
                }

                if (resource.Name.Length > name.Length
                    && resource.Name.IndexOf(name, StringComparison.CurrentCultureIgnoreCase) == 0
                    && resource.Name[name.Length] == '/')
                {
                    return false;
                }

                if (name.Length > resource.Name.Length
                    && name.IndexOf(resource.Name, StringComparison.CurrentCultureIgnoreCase) == 0
                    && name[resource.Name.Length] == '/')
                {
                    return false;
                }

                string[] pathNames = resource.Name.Split('/');
                for (int i = 0; i < foundPathNames.Length - 1 && i < pathNames.Length - 1; i++)
                {
                    if (foundPathNames[i].ToLower() != pathNames[i].ToLower())
                    {
                        break;
                    }

                    if (foundPathNames[i] != pathNames[i])
                    {
                        return false;
                    }
                }
            }

            return true;
        }
    }
}